package com.googlecode.phpunit4eclipse.j2phpunit.runner;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;

import org.junit.ComparisonFailure;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.InitializationError;

import com.googlecode.phpunit4eclipse.j2phpunit.parser.PHPMap;
import com.googlecode.phpunit4eclipse.j2phpunit.parser.PHPObject;
import com.googlecode.phpunit4eclipse.j2phpunit.parser.PHPSerializedParser;

public class PHPUnitRunner extends ParentRunner<Class<?>> {

	public static final String PHPUNIT_ARGS = "phpunitArgs";

	public static final String WORKING_DIRECTORY = "workingDirectory";

	private PHPDriver phpDriver;
	
	private Map<Class<?>, Description> listOfTests;
	
	private Map<String, Description> toBeTested;
	
	public PHPUnitRunner(Class<?> testClass) throws InitializationError {
		super(testClass);
		String workingDirectory = System.getProperty(WORKING_DIRECTORY);
		if (workingDirectory == null || workingDirectory.length() == 0) {
			throw new RuntimeException("Please specify phpunit working directory with -D" +  WORKING_DIRECTORY);
		}
		if (!new File(workingDirectory).exists()) {
			throw new RuntimeException("Non existing working directory " + workingDirectory);
		}
		String args = System.getProperty(PHPUNIT_ARGS);
		if (args == null || args.length() == 0) {
			throw new RuntimeException("Please specify phpunit arguments, splitted by | with -D" + PHPUNIT_ARGS);
		}
		try {
			phpDriver = new PHPDriver(PHPDriverHelper.getInstance().getPhpExec(null), workingDirectory, args.split("\\|"), PHPDriverHelper.getInstance().getTimeout());
			String content = phpDriver.readLine();
			PHPObject suite = PHPObject.cast(PHPSerializedParser.parse(content));
			processSuite(suite);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	private List<PHPObject> getGroups(PHPObject object) {
		List<PHPObject> list = new ArrayList<PHPObject>();
		PHPMap groups = PHPMap.cast(object.get("groups"));
		for(Object group : groups.values()) {
			for(Object test : PHPMap.cast(group).values()) {
				list.add(PHPObject.cast(test));
			}
		}
		return list;
	}
	
	private String getUniqueId(PHPObject object) {
		return object.getClassName() + " " + object.getString("name");
	}
	
	private void checkId(String id) throws PHPRunnerException {
		if (toBeTested.get(id) != null) {
			throw new PHPRunnerException("Not unique id " + id);
		}
	}
	
	private void processSuite(PHPObject suite) throws CannotCompileException, PHPRunnerException {
		listOfTests = new HashMap<Class<?>, Description>();
		toBeTested = new HashMap<String, Description>();
		for(PHPObject clazzObject : getGroups(suite)) {
			String id = getUniqueId(clazzObject);
			checkId(id);
			String name = clazzObject.getString("name");
			ClassPool classPool = ClassPool.getDefault();
			CtClass clazz = classPool.makeClass("junit.php." + name);
			Class<?> compiledClazz = clazz.toClass();
			Description description = Description.createSuiteDescription(name);
			for(PHPObject testObject : getGroups(clazzObject)) {
				String testId = getUniqueId(testObject);
				checkId(testId);
				String testName = testObject.getString("name");
				Description testDescription = Description.createTestDescription(compiledClazz, testName);
				description.addChild(testDescription);
				toBeTested.put(testId, testDescription);
			}
			listOfTests.put(compiledClazz, description);
			toBeTested.put(id, description);
		}
	}

	@Override
	protected Description describeChild(Class<?> clazz) {
		Description description = listOfTests.get(clazz); 
		return description;
	}
	
	@Override
	protected List<Class<?>> getChildren() {
		return new ArrayList<Class<?>>(listOfTests.keySet());
	}
	
	private Throwable createException(PHPObject exception) {
		Object cf = exception.get("comparisonFailure");
		if (cf != null && cf instanceof PHPObject) {
			PHPObject e  = PHPObject.cast(cf);
			PHPMap array = PHPMap.cast(e.get("\u0000Exception\u0000trace"));
			List<StackTraceElement> list = new ArrayList<StackTraceElement>();
			for(int i = 0;; i ++) {
				PHPMap ee = PHPMap.cast(array.get(Integer.toString(i)));
				if (ee == null) {
					break;
				}
				String function = ee.getString("function");
				String clazz = ee.getString("class");
				String file = ee.getString("file");
				Integer line = ee.getInteger("line");
				if ("invoke".equals(function) && "ReflectionMethod".equals(clazz) && file != null && file.endsWith("TestCase.php")) {
					break;
				}
				list.add(new StackTraceElement(
					clazz == null ? "?" : clazz,
					function == null ? "?" : function,
					file == null ? "?" : file,
					line == null ? -1 : line));
			}
			if ("PHPUnit_Framework_ComparisonFailure_String".equals(e.getClassName())) {
				Throwable t = new ComparisonFailure(e.getString("message"), e.getString("expected"), e.getString("actual"));
				t.setStackTrace(list.toArray(new StackTraceElement[]{}));
				return t;
			}
		}
		return new RuntimeException("Test failed with unmanaged exception");
	}

	@Override
	protected void runChild(Class<?> clazz, RunNotifier notifier) {
		notifier.fireTestStarted(listOfTests.values().iterator().next());
		try {
			while(true) {
				String line = phpDriver.readLine();
				if (line == null) {
					phpDriver.close();
					return;
				}
				if ("End.".equals(line)) {
					phpDriver.close();
					break;
				}
				PHPMap array = PHPMap.cast(PHPSerializedParser.parse(line));
				String eventName = array.getString("0");
				if ("startTestSuite".equals(eventName) || "startTest".equals(eventName)) {
					String id = getUniqueId(PHPObject.cast(array.get("1")));
					Description description = toBeTested.get(id);
					if (description != null) {
						notifier.fireTestStarted(description);
					}
				}
				else if ("endTestSuite".equals(eventName) || "endTest".equals(eventName)) {
					String id = getUniqueId(PHPObject.cast(array.get("1")));
					Description description = toBeTested.get(id);
					if (description != null) {
						notifier.fireTestFinished(description);
						toBeTested.remove(id);
					}
				}
				else if ("addFailure".equals(eventName) ) {
					String id = getUniqueId(PHPObject.cast(array.get("1")));
					Description description = toBeTested.get(id);
					if (description != null) {
						Throwable exception = createException(PHPObject.cast(array.get("2")));
						Failure failure = new Failure(description, exception);
						notifier.fireTestFailure(failure);
						toBeTested.remove(id);
					}
				}
				else {
					System.out.println("Ignored event : "+ eventName);
				}
			}
		}
		catch(IOException e) {
			// Stream closed
		}
		catch(Exception e) {
			throw new RuntimeException(e);
		}
		for(String id : toBeTested.keySet()) {
			Failure failure = new Failure(toBeTested.get(id), new RuntimeException("No confirmation from PHPUnit for this test"));
			notifier.fireTestFailure(failure);
		}
	}

}
